/**
 * LocationAPITools library, version 1.0.2
 * 
 * WWW: http://code.google.com/p/locationapitools
 *   
 * Summary:
 *   A collection of classes that simplify the development of location-aware mobile
 *   applications using the JSR-179 Location API on the J2ME (CLDC/MIDP) platform,
 *   e.g. on GPS-equipped smartphones.
 * 
 * Author:
 *   Matthias Stevens (Sony CSL Paris / Vrije Universiteit Brussel)
 *   Contact: matthias.stevens@gmail.com
 *  
 * Description:
 *   The primary use case is a location tracker that stores tracks in KML
 *   (cfr. Google Earth/Maps), CSV or plain text format.
 * 
 *   Package "com.sony.csl.j2me.locationapitools.demo" contains a MIDlet class that
 *   demonstrates how LocationAPITools can be used to create simple GPS tracker
 *   application that produces KML files. For examples of more advanced uses of this
 *   library refer to the NoiseTube project (see below, look for "NoiseTube MobileNoiseTagger").
 * 
 *   The code is known to be working on the following Symbian OS-based platforms:
 *     * S60 3rd Edition Feature Pack 1 (Symbian OS 9.2), e.g.: Nokia N95 and Nokia N95 8GB smartphones
 *     * S60 3rd Edition Feature Pack 2 (Symbian OS 9.3) 
 *   Although this is untested, the code should also work on other (non-Symbian/S60)
 *   J2ME CLDC/MIDP platforms/devices, as long as the JSR-179 Location API is supported
 *   on the platform/device in question.
 * 
 *   This library was developed within the scope of the NoiseTube project at
 *   Sony Computer Science Laboratory (CSL) Paris, for more information please refer to:
 *     * http://noisetube.net
 *     * http://code.google.com/p/noisetube
 *     * http://www.csl.sony.fr 
 *    
 * License: 
 *   Copyright 2008-2009 Sony CSL Paris
 *   Licensed under the Apache License, Version 2.0 (the "License"); you may not use
 *   this file except in compliance with the License. You may obtain a copy of the
 *   License at http://www.apache.org/licenses/LICENSE-2.0.
 *   Unless required by applicable law or agreed to in writing, software distributed
 *   under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 *   CONDITIONS OF ANY KIND, either express or implied. See the License for the
 *   specific language governing permissions and limitations under the License.
 */

package com.sony.csl.j2me.locationapitools;

import java.util.Vector;

import javax.microedition.location.*;


/**
 * LocationProxy acts as a proxy (middleman) between a JSR-179 (Location API) LocationProvider object
 * and (multiple!) objects (instances of classes that implement the LocationProxyListener interface) that
 * need access to location information.
 * This is necessary because LocationProvider only supports a single listener object. 
 * 
 * @author mstevens (Sony CSL Paris / Vrije Universiteit Brussel)
 *
 */
public class LocationProxy implements LocationListener
{
	//static members:
	static public int LOCATIONLISTENER_INTERVAL = 12;
	static public int LOCATIONLISTENER_TIMEOUT = 3;
	static public int LOCATIONLISTENER_MAXAGE = 8;
	
	static public Criteria DEFAULT_LOCATIONPROVIDER_CRITERIA;
	
	static
	{
		DEFAULT_LOCATIONPROVIDER_CRITERIA = new Criteria();
		DEFAULT_LOCATIONPROVIDER_CRITERIA.setHorizontalAccuracy(Criteria.NO_REQUIREMENT);
		DEFAULT_LOCATIONPROVIDER_CRITERIA.setVerticalAccuracy(Criteria.NO_REQUIREMENT);
		DEFAULT_LOCATIONPROVIDER_CRITERIA.setPreferredResponseTime(Criteria.NO_REQUIREMENT);
		DEFAULT_LOCATIONPROVIDER_CRITERIA.setPreferredPowerConsumption(Criteria.POWER_USAGE_HIGH);
		DEFAULT_LOCATIONPROVIDER_CRITERIA.setCostAllowed(false); // allowed to cost
		DEFAULT_LOCATIONPROVIDER_CRITERIA.setSpeedAndCourseRequired(false);
		DEFAULT_LOCATIONPROVIDER_CRITERIA.setAltitudeRequired(false);
		DEFAULT_LOCATIONPROVIDER_CRITERIA.setAddressInfoRequired(false);
	}
	
	private static LocationProxy INSTANCE = null;
	
	//instance members:
	private Criteria providerCriteria = DEFAULT_LOCATIONPROVIDER_CRITERIA;
	private int listeningInterval = LOCATIONLISTENER_INTERVAL;
	private int listeningTimeout = LOCATIONLISTENER_TIMEOUT;
	private int listeningMaxAge = LOCATIONLISTENER_MAXAGE;
	private LocationProvider provider = null;
	private Vector listeners;
	private boolean firstLocationUpdatePassed;
	private boolean firstValidLocationUpdatePassed;
	private Location lastValidLocation;
	private int numberOfLocations;
	private int numberOfValidLocations;
	private boolean waitingForLocation;
	
	
	private LocationProxy()
	{			
		listeners = new Vector();
		numberOfLocations = 0;
		numberOfValidLocations = 0;
		waitingForLocation = false;
	}
	
	/**
	 * Provide singleton instance of this class.
	 * 
	 * @return static instance of this class.
	 */
	public static LocationProxy getInstance() throws Exception
	{
		if(INSTANCE == null)
		{
			//Enable use of this class when Location API is supported.
			if(LocationAPIUtilities.isLocationApiSupported())
				INSTANCE = new LocationProxy();
			else
				throw new Exception("Location API is not supported on this device");
		}
		return INSTANCE;
	}
	
	public void addListener(LocationProxyListener listener)
	{
		listeners.addElement(listener);
	}
	
	public void removeListener(LocationProxyListener listener)
	{
		listeners.removeElement(listener);
	}
	
	public void removeAllListeners()
	{
		listeners.removeAllElements();
	}
	
	/**
	 * @return providerCriteria
	 */
	public Criteria getProviderCriteria()
	{
		return providerCriteria;
	}

	/**
	 * @return listeningInterval
	 */
	public int getListeningInterval()
	{
		return listeningInterval;
	}

	/**
	 * @return listeningTimeout
	 */
	public int getListeningTimeout()
	{
		return listeningTimeout;
	}

	/**
	 * @return listeningMaxAge
	 */
	public int getListeningMaxAge()
	{
		return listeningMaxAge;
	}

	/**
	 * @return connected
	 */
	public boolean isConnected()
	{
		return (provider != null && provider.getState() == LocationProvider.AVAILABLE);
	}
	
	/**
	 * @return provider
	 */
	public LocationProvider getProvider()
	{
		return provider;
	}
	
	/**
	 * @return state of the provider
	 */
	public int getProviderState()
	{
		if(isConnected())
			return provider.getState();
		else
			return -1;
	}
	
	/**
	 * @return the numberOfLocations
	 */
	public int getNumberOfLocations()
	{
		return numberOfLocations;
	}

	/**
	 * @return the numberOfValidLocations
	 */
	public int getNumberOfValidLocations()
	{
		return numberOfValidLocations;
	}

	public boolean connect() throws Exception
	{
		return connect(DEFAULT_LOCATIONPROVIDER_CRITERIA, LOCATIONLISTENER_INTERVAL, LOCATIONLISTENER_TIMEOUT, LOCATIONLISTENER_MAXAGE);
	}
	
	public boolean connect(Criteria criteria) throws Exception
	{
		return connect(criteria, LOCATIONLISTENER_INTERVAL, LOCATIONLISTENER_TIMEOUT, LOCATIONLISTENER_MAXAGE);
	}
	
	public boolean connect(Vector criteriaVector) throws Exception
	{
		return connect(criteriaVector, LOCATIONLISTENER_INTERVAL, LOCATIONLISTENER_TIMEOUT, LOCATIONLISTENER_MAXAGE);
	}
	
	/**
	 * Attempt to connect the proxy instance to a provider that is to be selected according to one of the criteria in the vector.
	 * The criteria are tried in the order in which they appear in the vector. The interval, timeout and maxAge
	 * parameters are used to connect to the first provider that is found.
	 * 
	 * @param criteriaVector
	 * @param interval
	 * @param timeout
	 * @param maxAge
	 * @return boolean value indicating the success (true) or failure (false) of the connection attempt
	 * @throws Exception
	 */
	public boolean connect(Vector criteriaVector, int interval, int timeout, int maxAge) throws Exception
	{
		disconnect();
		int c = 0;
		while(c < criteriaVector.size() && !connect((Criteria) criteriaVector.elementAt(c), interval, timeout, maxAge))
			c++;
		if(!isConnected())
			//A suitable LocationProvider was acquired (the proxy is connected)
			return true;
		else
			//Could not acquire a suitable LocationProvider
			return false;
	}
	
	/**
	 * Attempt to connect the proxy instance to a provider that is to be selected according to the supplied criteria.
	 * If a suitable provider is found the interval, timeout and maxAge parameters are used to connect to it.
	 * 
	 * @param criteria
	 * @param interval
	 * @param timeout
	 * @param maxAge
	 * @return boolean value indicating the success (true) or failure (false) of the connection attempt
	 * @throws Exception
	 */
	public boolean connect(Criteria criteria, int interval, int timeout, int maxAge) throws Exception
	{
		if(!isConnected())
		{
			disconnect();
			numberOfLocations = 0;
			numberOfValidLocations = 0;
			waitingForLocation = false;
			try
			{
				provider = LocationProvider.getInstance(criteria);
			}
			catch(LocationException e)
			{
				throw new Exception("All supported LocationProviders are out of service");
			}
			try
			{
				if (provider != null && provider.getState() == LocationProvider.AVAILABLE)
				{	//A suitable LocationProvider was acquired, register the proxy as a listener of the provider and inform the listeners of the proxy
					firstLocationUpdatePassed = false;
					firstValidLocationUpdatePassed = false;
					provider.setLocationListener(this, interval, timeout, maxAge); //!!!
					//Store the parameters for this connection:
					providerCriteria = criteria;
					listeningInterval = interval;
					listeningTimeout = timeout;
					listeningMaxAge = maxAge;
					//Inform the listeners:
					for(int i = 0; i < listeners.size(); i++)
						((LocationProxyListener) listeners.elementAt(i)).proxyConnected(this, provider);
					return true;
				}
				else
					//Could not acquire a suitable and available LocationProvider
					return false;
			}
			catch(IllegalArgumentException e)
			{	//illegal interval/timeout/maxage arguments:
				throw e;
			}
			catch(Exception e)
			{	//other exception
				return false;
			}
		}
		else
			throw new Exception("LocationProxy is already connected to a LocationProvider, first call disconnect().");
	}
	
	public void disconnect()
	{
		if(isConnected())
		{
			try
			{
				provider.setLocationListener(null, listeningInterval, listeningTimeout, listeningMaxAge);
			}
			catch(Exception e) {} //exception can be ignored here
			provider.reset();
			provider = null;
			//Inform the listeners:
			for(int i = 0; i < listeners.size(); i++)
				((LocationProxyListener) listeners.elementAt(i)).proxyDisconnected(this);
			System.gc();
		}
	}
	
	/** 
	 * @see javax.microedition.location.LocationListener#locationUpdated(javax.microedition.location.LocationProvider, javax.microedition.location.Location)
	 */
	public void locationUpdated(LocationProvider provider, Location location)
	{
		numberOfLocations++;
		if(location.isValid())
		{
			numberOfValidLocations++;
			lastValidLocation = location;
			if(waitingForLocation)
			{
				waitingForLocation = false;
				synchronized(this)
				{
					this.notify();
				}
			}
		}
		if(!firstLocationUpdatePassed)
		{	//This is the first location update, inform the listeners
			for(int i = 0; i < listeners.size(); i++)
			{
				LocationProxyListener listener = (LocationProxyListener) listeners.elementAt(i);
				if(location.isValid() || listener.receivesInvalidLocations())
					listener.firstLocationFound(this, provider, location);
			}
			firstLocationUpdatePassed = true;
			if(location.isValid())
				firstValidLocationUpdatePassed = true;
		}
		else if((firstLocationUpdatePassed && !firstValidLocationUpdatePassed) && location.isValid()) //first location update was invalid and the current location is valid
		{	//This is the first location update, inform the listeners
			for(int i = 0; i < listeners.size(); i++)
			{
				LocationProxyListener listener = (LocationProxyListener) listeners.elementAt(i);
				if(!listener.receivesInvalidLocations()) //only send it to listeners that did not get a possible earlier invalid location
					listener.firstLocationFound(this, provider, location);
			}
			firstValidLocationUpdatePassed = true;
		}
		//Forward the update to all listeners:
		for(int i = 0; i < listeners.size(); i++)
		{
			LocationProxyListener listener = (LocationProxyListener) listeners.elementAt(i);
			if(location.isValid() || listener.receivesInvalidLocations())
				listener.locationUpdated(this, provider, location);
		}
	}

	/** 
	 * @see javax.microedition.location.LocationListener#providerStateChanged(javax.microedition.location.LocationProvider, int)
	 */
	public void providerStateChanged(LocationProvider provider, int newState)
	{
		//Forward the state change to all listeners:
		for(int i = 0; i < listeners.size(); i++)
			((LocationProxyListener) listeners.elementAt(i)).locationProviderStateChanged(this, provider, newState);
	}
	
	/**
	 * @return the lastValidLocation
	 */
	public synchronized Location getLastValidLocation()
	{
		return lastValidLocation;
	}

	public Location getCurrentLocation(int timeoutSeconds)
	{
		return getCurrentLocation(timeoutSeconds, false);
	}
	
	public Location getCurrentLocation(int timeoutSeconds, boolean pausableThread)
	{	
		if(pausableThread)
		{	//pauses the current thread and waits for a new (valid) location to arrive through the LocationListener functionality
			waitingForLocation = true;
			try
			{
				while(waitingForLocation)
				{
					synchronized(this)
					{
						this.wait(timeoutSeconds * 1000);
					}
				}
			}
			catch(Exception e)
			{
				return null;
			}
			return lastValidLocation;
		}
		else
		{	//directly requests a location from the provider (a confirmation warning will pop-up in unsigned MIDlets)
			try
			{
				return provider.getLocation(timeoutSeconds);
			}
			catch(Exception e)
			{
				return null;
			}
		}
	}
	
}
